SPB Git

Repository Public

page.tsx 4.6 KB 59 lines tsx

1import type { Metadata } from "next";2import Link from "next/link";3import { FilmGrid } from "@/components/FilmCard";4import { listFilms, listSeries, listTags } from "@/lib/films";5import { dict, href, type Locale } from "@/lib/i18n";67export async function generateMetadata({ params }: { params: Promise<{ locale: string }> }): Promise<Metadata> {8  const locale = (await params).locale as Locale;9  return { title: dict(locale).explore.title, description: dict(locale).explore.sub, alternates: { canonical: href(locale, "/explore"), languages: { en: "/explore", fr: "/fr/explore" } } };10}1112type SP = Record<string, string | undefined>;13const Chip = ({ on, to, children }: { on: boolean; to: string; children: React.ReactNode }) => <Link href={to} scroll={false} aria-current={on ? "true" : undefined} className="chip">{children}</Link>;14const Group = ({ label, children }: { label: string; children: React.ReactNode }) => (15  <div className="grid gap-2 sm:grid-cols-[110px_1fr] sm:items-start"><p className="label pt-2">{label}</p><div className="flex flex-wrap gap-2">{children}</div></div>16);17const KEYS = ["series", "tag", "lang", "kind", "max", "sort"] as const;1819export default async function Explore({ params, searchParams }: { params: Promise<{ locale: string }>; searchParams: Promise<SP> }) {20  const locale = (await params).locale as Locale;21  const sp = await searchParams;22  const t = dict(locale);23  const f = { series: sp.series, tag: sp.tag, lang: ["en", "fr", "bilingual"].includes(sp.lang ?? "") ? sp.lang : undefined, kind: ["fiction", "factual", "experimental"].includes(sp.kind ?? "") ? sp.kind : undefined, max: sp.max === "30" || sp.max === "45" ? sp.max : undefined, sort: sp.sort === "top" ? "top" : undefined };24  const [{ films }, series, tags] = await Promise.all([25    listFilms(locale, { series: f.series, tag: f.tag, language: f.lang, kind: f.kind, maxDuration: f.max ? Number(f.max) : undefined, sort: f.sort as "top" | undefined, limit: 48 }),26    listSeries(locale), listTags(locale),27  ]);28  const link = (patch: Partial<Record<(typeof KEYS)[number], string | undefined>>) => {29    const q = new URLSearchParams();30    for (const k of KEYS) { const v = k in patch ? patch[k] : f[k]; if (v) q.set(k, v); }31    const s = q.toString();32    return href(locale, `/explore${s ? `?${s}` : ""}`);33  };34  const active = KEYS.some((k) => f[k]);35  const kinds = [...new Set(tags.map((x) => x.kind))];3637  return (38    <div className="py-10">39      <h1 className="display text-5xl md:text-6xl">{t.explore.title}</h1>40      <p className="prose-film mt-3 text-lg">{t.explore.sub}</p>41      <div className="mt-8 space-y-4 border-y border-[var(--color-line-soft)] py-6">42        <Group label={t.nav.series}><Chip on={!f.series} to={link({ series: undefined })}>{t.explore.all}</Chip>{series.filter((s) => s.count > 0).map((s) => <Chip key={s.slug} on={f.series === s.slug} to={link({ series: f.series === s.slug ? undefined : s.slug })}>{s.title}</Chip>)}</Group>43        <Group label={t.explore.form}>{(["fiction", "factual", "experimental"] as const).map((k) => <Chip key={k} on={f.kind === k} to={link({ kind: f.kind === k ? undefined : k })}>{t.film[k]}</Chip>)}</Group>44        {kinds.map((kind) => (45          <Group key={kind} label={t.tagKinds[kind] ?? kind}>{tags.filter((x) => x.kind === kind).map((x) => <Chip key={x.slug} on={f.tag === x.slug} to={link({ tag: f.tag === x.slug ? undefined : x.slug })}>{x.label} <span className="ml-1 opacity-60">{x.count}</span></Chip>)}</Group>46        ))}47        <Group label={t.explore.language}>{(["en", "fr", "bilingual"] as const).map((k) => <Chip key={k} on={f.lang === k} to={link({ lang: f.lang === k ? undefined : k })}>{t.langs[k]}</Chip>)}</Group>48        <Group label={t.explore.length}><Chip on={f.max === "30"} to={link({ max: f.max === "30" ? undefined : "30" })}>{t.explore.under30}</Chip><Chip on={f.max === "45"} to={link({ max: f.max === "45" ? undefined : "45" })}>{t.explore.under45}</Chip></Group>49        <Group label={t.explore.sort}><Chip on={!f.sort} to={link({ sort: undefined })}>{t.explore.latest}</Chip><Chip on={f.sort === "top"} to={link({ sort: "top" })}>{t.explore.top}</Chip></Group>50      </div>51      <div className="mt-6 flex items-center justify-between text-sm text-[var(--color-faint)]">52        <p aria-live="polite">{films.length} {t.explore.results}</p>53        {active && <Link href={href(locale, "/explore")} className="underline hover:text-[var(--color-text)]">{t.explore.clear}</Link>}54      </div>55      <div className="mt-5">{films.length ? <FilmGrid films={films} locale={locale} /> : <p className="card p-8 text-[var(--color-muted)]">{t.explore.none}</p>}</div>56    </div>57  );58}59
Files main
Menu

Theme